library local_datetime;

import 'dart:math';

enum Week {
  sunday,
  monday,
  tuesday,
  wednesday,
  thursday,
  friday,
  saturday,
}

class LocalDate implements Comparable<LocalDate> {
  DateTime date;

  Week startOfWeek;

  /// 定义的星期最后一天的名称
  /// 根据{@link startOfWeek}决定
  Week get endOfWeek {
    var number = Week.values.indexOf(startOfWeek);
    return Week.values[(number + 6) % 7];
  }

  List<Week> get weeksOfStartOfWeek {
    var firstIndex = Week.values.indexOf(startOfWeek);
    final weeks = <Week>[];
    for (var i = 0; i < 7; i++) {
      weeks.add(Week.values[(firstIndex + i) % 7]);
    }
    return weeks;
  }

  static var defaultStartOfWeek = Week.sunday;

  LocalDate(this.date, [Week? startOfWeek])
      : startOfWeek = startOfWeek ?? defaultStartOfWeek;

  LocalDate.now([Week? startOfWeek]) : this(DateTime.now(), startOfWeek);

  LocalDate.of(int year, [int month = 1, int day = 1, Week? startOfWeek])
      : date = DateTime(year, month, day),
        startOfWeek = startOfWeek ?? defaultStartOfWeek;

  LocalDate.ofTimestamp(int timestamp, [Week? startOfWeek])
      : this(DateTime.fromMillisecondsSinceEpoch(timestamp), startOfWeek);

  LocalDate.ofString(String s, [Week? startOfWeek])
      : this(DateTime.parse(s), startOfWeek);

  LocalDate.ofLocalDate(LocalDate localDate)
      : this(localDate.date, localDate.startOfWeek);

  LocalDate.ofDateTime(DateTime dateTime, [Week? startOfWeek])
      : this(dateTime, startOfWeek);

  int get year => date.year;

  set year(int year) {
    date = DateTime(year, month, day);
  }

  int get day => date.day;

  set day(int day) {
    date = DateTime(year, month, day);
  }

  int get weekOfMonth {
    var firstDayOfMonth = this.firstDayOfMonth();
    return (max(0, day + weeksOfStartOfWeek.indexOf(firstDayOfMonth.week)) / 7)
        .ceil();
  }

  set month(int month) {
    date = DateTime(year, month, day);
  }

  int get month => date.month;

  Week get week => Week.values[date.weekday % 7];

  set week(Week week) {
    date = withDayOfWeek(week).date;
  }

  int get daysInMonth {
    return DateTime(year, month + 1).difference(DateTime(year, month)).inDays;
  }

  int get daysInYear {
    return DateTime(year + 1).difference(DateTime(year)).inDays;
  }

  int get weekOfYear {
    var firstDayOfYear = this.firstDayOfYear();
    return (max(0,
                dayOfYear + weeksOfStartOfWeek.indexOf(firstDayOfYear.week)) /
            7)
        .ceil();
  }

  LocalDate firstDayOfYear() {
    return LocalDate.of(year);
  }

  LocalDate lastDayOfYear() {
    return LocalDate.of(year + 1).minusDay(1);
  }

  /// 本周的第一天
  /// @returns {LocalDate} 新的LocalDate
  LocalDate firstDayOfWeek() {
    return withDayOfWeek(startOfWeek);
  }

  /// 本周的最后一天
  /// @returns {LocalDate} 新的LocalDate
  LocalDate lastDayOfWeek() {
    return withDayOfWeek(endOfWeek);
  }

  /// 本月的第一天
  /// @return {LocalDate} 新的对象
  LocalDate firstDayOfMonth() {
    return withDayOfMonth(1);
  }

  /// 本月最后一天
  /// @return {LocalDate} 新的对象
  LocalDate lastDayOfMonth() {
    return withDayOfMonth(maxDayOfMonth());
  }

  /// 本月的第n天
  /// @param n {number} 1-31
  /// @return {LocalDate} 返回新的LocalDate
  LocalDate withDayOfMonth(int n) {
    return copy()..day = n;
  }

  /// @return 当前是本年的第几天
  int get dayOfYear {
    return DateTime(year, month, day).difference(firstDayOfYear().date).inDays +
        1;
  }

  /// @returns {number} 当前月总共有多少天
  int maxDayOfMonth() {
    return daysInMonth;
  }

  /// @return 当前年份有多少天数
  int maxDayOfYear() {
    return isLeapYear() ? 366 : 355;
  }

  /// 当前年份是否是闰年
  bool isLeapYear() {
    return year % 400 == 0 || (year % 4 == 0 && year % 100 != 0);
  }

  /// 按照星期称呼找到本周的那一天
  /// @param week
  LocalDate withDayOfWeek(Week week) {
    var weeks = weeksOfStartOfWeek;
    return minusDay(weeks.indexOf(this.week) - weeks.indexOf(week));
  }

  /// 以当前日期减去一个天数
  /// @param day 减少天数
  /// @return 返回一个新的{@link LocalDate}
  LocalDate minusDay(int day) {
    var copy = this.copy();
    copy.day -= day;
    return copy;
  }

  /// 以当前日期减去一个月数
  /// @param month 减少月数
  /// @return 返回一个新的{@link LocalDate}
  LocalDate minusMonth(int month) {
    var copy = this.copy();
    copy.month -= month;
    return copy;
  }

  /// 以当前日期减去一个年数
  /// @param year 减少年数
  /// @return 返回一个新的{@link LocalDate}
  LocalDate minusYear(int year) {
    var copy = this.copy();
    copy.year -= year;
    return copy;
  }

  /// 以当前日期减去一个周数
  /// @param week 减少周数
  /// @return 返回一个新的{@link LocalDate}
  LocalDate minusWeek(int week) {
    var copy = this.copy();
    copy.day -= 7 * week;
    return copy;
  }

  /// 复制一个新的实例
  LocalDate copy() {
    return LocalDate.ofLocalDate(this);
  }

  LocalDate plusDay(int day) {
    var copy = this.copy();
    copy.day += day;
    return copy;
  }

  /// 以当前日期减去一个月数
  /// @param month 减少月数
  /// @return 返回一个新的{@link LocalDate}
  LocalDate plusMonth(int month) {
    var copy = this.copy();
    copy.month += month;
    return copy;
  }

  /// 以当前日期减去一个年数
  /// @param year 减少年数
  /// @return 返回一个新的{@link LocalDate}
  LocalDate plusYear(int year) {
    var copy = this.copy();
    copy.year += year;
    return copy;
  }

  /// 以当前日期减去一个周数
  /// @param week 减少周数
  /// @return 返回一个新的{@link LocalDate}
  LocalDate plusWeek(int week) {
    var copy = this.copy();
    copy.day += 7 * week;
    return copy;
  }

  @override
  int compareTo(LocalDate other) {
    return date.compareTo(other.date);
  }

  @override
  bool operator ==(Object other) {
    return identical(this, other) ||
        (other is LocalDate && hashCode == other.hashCode);
  }

  bool operator >(Object object) {
    return hashCode > object.hashCode;
  }

  bool operator <(Object object) {
    return hashCode < object.hashCode;
  }

  bool operator <=(Object object) {
    return hashCode <= object.hashCode;
  }

  bool operator >=(Object object) {
    return hashCode >= object.hashCode;
  }

  /// 按照指定的模式格式化日期
  /// @param template 模式
  /// 支持 y - 年
  ///     M - 月
  ///     W - 周
  ///     d - 日
  String format([String template = "yyyy-MM-dd"]) {
    final map = <String, String>{
      "yyyy": year.toString(),
      "MM": fill0(month.toString()),
      "M": month.toString(),
      "WW": fill0(week.toString()),
      "W": week.toString(),
      "dd": fill0(day.toString()),
      "d": day.toString(),
    };
    for (var entry in map.entries) {
      final regex = entry.key;
      final value = entry.value;
      template = template.replaceAll(regex, value);
    }
    return template;
  }

  @override
  int get hashCode => DateTime(year, month, day).microsecondsSinceEpoch;

  @override
  String toString() {
    return this.format();
  }
}

class LocalDateTime extends LocalDate {
  LocalDateTime.of(int year,
      [int month = 1,
      int day = 1,
      int hour = 0,
      int minute = 0,
      int second = 0,
      int millisecond = 0,
      int microsecond = 0,
      Week? startOfWeek])
      : super(
            DateTime(year, month, day, hour, second, millisecond, microsecond),
            startOfWeek);

  LocalDateTime(DateTime date, [Week? startOfWeek]) : super(date, startOfWeek);

  LocalDateTime.now([Week? startOfWeek]) : this(DateTime.now(), startOfWeek);

  LocalDateTime.ofTimestamp(int timestamp, [Week? startOfWeek])
      : this(DateTime.fromMillisecondsSinceEpoch(timestamp), startOfWeek);

  LocalDateTime.ofString(String s, [Week? startOfWeek])
      : this(DateTime.parse(s), startOfWeek);

  LocalDateTime.ofLocalDateTime(LocalDateTime localDate)
      : this(localDate.date, localDate.startOfWeek);

  LocalDateTime.ofDateTime(DateTime dateTime, [Week? startOfWeek])
      : this(dateTime, startOfWeek);

  int get clock => hour < 12 ? hour : hour - 12;

  int get hour => date.hour;

  set hour(int hour) {
    date = DateTime(
        year, month, day, hour, minute, second, millisecond, microsecond);
  }

  int get minute => date.minute;

  set minute(int day) {
    date = DateTime(
        year, month, day, hour, minute, second, millisecond, microsecond);
  }

  int get second => date.second;

  set second(int second) {
    date = DateTime(
        year, month, day, hour, minute, second, millisecond, microsecond);
  }

  int get millisecond => date.millisecond;

  set millisecond(int millisecond) {
    date = DateTime(
        year, month, day, hour, minute, second, millisecond, microsecond);
  }

  int get microsecond => date.month;

  set microsecond(int microsecond) {
    date = DateTime(
        year, month, day, hour, minute, second, millisecond, microsecond);
  }

  LocalDateTime plusHour(int hour) {
    var copy = this.copy();
    copy.hour += hour;
    return copy;
  }

  LocalDateTime plusMinute(int minute) {
    var copy = this.copy();
    copy.minute += minute;
    return copy;
  }

  LocalDateTime plusSecond(int second) {
    var copy = this.copy();
    copy.second += second;
    return copy;
  }

  LocalDateTime plusMillisecond(int millisecond) {
    var copy = this.copy();
    copy.millisecond += millisecond;
    return copy;
  }

  LocalDateTime plusMicrosecond(int microsecond) {
    var copy = this.copy();
    copy.microsecond -= microsecond;
    return copy;
  }

  LocalDateTime minusHour(int hour) {
    var copy = this.copy();
    copy.hour -= hour;
    return copy;
  }

  LocalDateTime minusMinute(int minute) {
    var copy = this.copy();
    copy.minute -= minute;
    return copy;
  }

  LocalDateTime minusSecond(int second) {
    var copy = this.copy();
    copy.second -= second;
    return copy;
  }

  LocalDateTime minusMillisecond(int millisecond) {
    var copy = this.copy();
    copy.millisecond -= millisecond;
    return copy;
  }

  LocalDateTime minusMicrosecond(int microsecond) {
    var copy = this.copy();
    copy.microsecond -= microsecond;
    return copy;
  }

  @override
  LocalDateTime plusDay(int day) {
    var copy = this.copy();
    copy.day += day;
    return copy;
  }

  @override
  bool operator ==(Object other) {
    return identical(this, other) ||
        (other is LocalDateTime && hashCode == other.hashCode);
  }

  @override
  LocalDateTime copy() {
    return LocalDateTime.ofLocalDateTime(this);
  }

  @override
  int get hashCode => date.microsecondsSinceEpoch;

  LocalDate toLocalDate() {
    return LocalDate.ofDateTime(date, startOfWeek);
  }

  @override
  String format([String template = "yyyy-MM-dd HH:mm:ss"]) {
    template = super.format(template);
    final map = <String, String>{
      "HH": fill0(hour.toString()),
      "H": hour.toString(),
      "hh": fill0(clock.toString()),
      "h": clock.toString(),
      "mm": fill0(minute.toString()),
      "m": minute.toString(),
      "ss": fill0(second.toString()),
      "s": second.toString(),
      "SS": fill0(millisecond.toString(), 3),
      "S": millisecond.toString(),
    };
    for (var entry in map.entries) {
      final regex = entry.key;
      final value = entry.value;
      template = template.replaceAll(regex, value);
    }
    return template;
  }

  @override
  String toString() {
    return date.toIso8601String();
  }

  @override
  bool operator <=(Object object) {
    return hashCode <= object.hashCode;
  }

  @override
  bool operator >=(Object object) {
    return hashCode >= object.hashCode;
  }
}

String fill0(String s, [int length = 2]) {
  return s.padLeft(length, "0");
}
